package com.troyhigh.njrotc.admintracker;

import java.sql.Date;
import java.util.ArrayList;

/**
 * The class that puts it all together, implements the database functions to provide
 * the working layer with which GUIs/command lines will work with the AdminTracker
 * to provide reliable and fast ribbon records. Yessir, this is the place.
 * 
 * I'm giving you the following methods:
 * addCadet/addEvent
 * createCadet/createEvent
 * findCadets/findEvents
 * removeCadet/removeEvent
 * outputAwards
 * calculateHowMuchToGive
 * transactCadetAward
 * 
 * @author Quan Nguyen
 * @version 2008-December-23
 */
public class AdminTracker
{
    public static final String RIBBON_LABELS = 
    "ma, du, dc, hc, ca, ua, ap, n4, n3, n2, "+
    "n1, ec, aa, ep, pf, pa, us, cs, dt, cg, "+
    "rt, oi, re, bl, sc"; //do not change this, because you're going to screw
    //with the indexing
    
    public static final String[] RIBBON_TITLES =
    {"NO RIBBON" ,"RIBBON ONLY", "ONE BRONZE", 
     "TWO BRONZE", "ONE SILVER", "TWO SILVER", 
     "ONE GOLD", "TWO GOLD",   "THREE GOLD"};
    
    /*This string binds the event to its roster. Changing this method may
     * cause the events to lose sight of their rosters and lose all roster data.
     */
    public static String rosterID(String name, Date date)
    {
         String namehash = (name.length() > 5) ? name.substring(0,5).toLowerCase() : name;
         String datehash = "" + (date.getTime() / 100000);
         String pid = namehash.trim().replace(" ", "_") + datehash;
         return pid;
    }
    
    private Class DB_MODEL;
    private Database database;
    
    private Class BU_MODEL;
    private Database backup;
    
    private boolean backupEnabled = true;
    private int backupInterval = 300000; //Backs up in (backupInterval) milliseconds
                                         //I.E. 60,000 is 60 seconds = 1 minute
    
    private Thread backupThread;
    
    public AdminTracker()
    {
        this(SQLDatabase.class, SERDatabase.class);
    }
    
    public AdminTracker(Class dbm, Class bum)
    {
        DB_MODEL = dbm;
        BU_MODEL = bum;
        try 
        {
            System.out.println("[constructor] Database model: " + DB_MODEL.toString());
            System.out.println("[constructor] Backup model: " + BU_MODEL.toString());
            database = ((Database) DB_MODEL.newInstance());
            backup = ((Database) BU_MODEL.newInstance());
        }
        catch (Exception e) 
        {
            System.out.println("[error] Databases failed to initialize into objects!");
            System.out.println("[error] AdminTracker can no longer continue.");
            System.exit(1);
        }
        database.connect();
        //backup.connect(); //Backup DOES NOT CONNECT. It only does the backup operation.
        startBackup();
    }
    
    private void startBackup()
    {
        backupThread = (new Thread()
        {
            @Override
            public void run()
            {
                try
                {
                    System.out.println("[backup] Backup initialized.");
                    while(backupEnabled && backup.canBackup())
                    {
                        sleep(backupInterval);
                        backup();
                    }
                    if(!backup.canBackup())
                    {
                        System.out.println("[error] Backup failed because " + BU_MODEL.toString() + "cannot back up!");
                    }
                }
                catch (InterruptedException e)
                {
                    System.out.println("[error] Backup operation interrupted!");
                }
                catch (ThreadDeath td)
                {
                    System.out.println("[backup term] The backup thread has been terminated.");
                }
            }
        });
        backupThread.start();
    }
    
    public void backup()
    {
        backup.setCadets(database.getCadets());
        backup.setEvents(database.getEvents());
        backup.backup();
        System.out.println("[backup] (System backed up.)");
    }
    
    public void restore(String filename)
    {
        backup.restore(filename);
        System.out.println("[restore] Backup information set to internal database.");
        System.out.println("[restore] Type \"useBackup\" to use this data.");
    }
    
    public void commit()
    {
        database.commit();
    }
    
    public void disconnect()
    {
        database.commit();
        database.disconnect();
        //backup.disconnect(); Backup never connected.
        backupThread.interrupt();
    }
    
    public ArrayList<Cadet> getCadets()
    {
        return database.getCadets();
    }
    
    public ArrayList<Event> getEvents()
    {
        return database.getEvents();
    }   
    
    public void copyBackup()
    {
        database.setCadets(backup.getCadets());
        database.setEvents(backup.getEvents());
    }
    
    public void addCadet(Cadet c)
    {
        ArrayList<Cadet> cadets = database.getCadets();
        cadets.add(c);
        database.setCadets(cadets);
    }
    
    public void addEvent(Event e)
    {
        ArrayList<Event> events = database.getEvents();
        events.add(e);
        database.setEvents(events);
    }
    
    public void createCadet(int p, String l, String f, int i, char g, int r, int n)
    {
        addCadet(new Cadet(p, l, f, i, g, r, n));
    }
    
    public void createEvent(String n, Date d, Category c, int t)
    {
        addEvent(new Event(n, d, c, t));
    }
    
    /* I find it better that I DON'T implement a way to get just one. I can
     * let whatever extends this abstract class implement whatever it wants to do
     * with the complete set of data.
     * 
     * This is a weird case because from this point, setting/editing/removing 
     * cadets and events can also be done by their fields, not just having an
     * object to .equals() to
    */
    
    public ArrayList<Cadet> findCadets(int i, String l, String f)
    {
        ArrayList<Cadet> results = new ArrayList<Cadet>();
        for(Cadet c : database.getCadets())
        {
            if( ( i > 0 ? c.getID() == i : true) &&
                ( !(l == null || l.equals("")) ? c.getLastName().equals(l) : true) &&
                ( !(f == null || f.equals("")) ? c.getFirstName().equals(f) : true) )
           results.add(c);
        }
        return results;
    }
    
    public ArrayList<Event> findEvents(String n, Date d)
    {
        ArrayList<Event> results = new ArrayList<Event>();
        for(Event e : database.getEvents())
        {
            if( !(n == null || n.equals("")) ? e.getName().equals(n) : true &&
                d != null ? e.getDate().equals(d) : true)
            results.add(e);
        }
        return results;
    }
    
    //These methods don't think twice. So think (read: implement) carefully!
    public void removeCadet(Cadet c)
    {
        ArrayList<Cadet> cadets = database.getCadets();
        int fidx = cadets.indexOf(c);
        if(fidx == -1)
        {
            System.out.println("[error] Cadet not found to remove!");
        }
        else
        {
            int lidx = cadets.lastIndexOf(c);
            if(fidx == lidx)
                cadets.remove(c);
            else
                System.out.println("[error] Cadet matches two indices in the database!");
            database.setCadets(cadets);
        }
    }
    
    public void removeEvent(Event e)
    {
        ArrayList<Event> events = database.getEvents();
        int fidx = events.indexOf(e);
        if(fidx == -1)
        {
            System.out.println("[error] Event not found to remove!");
        }
        else
        {
            int lidx = events.lastIndexOf(e);
            if(fidx == lidx)
                events.remove(e);
            else
                System.out.println("[error] Event matches two indices in the database!");
            database.setEvents(events);
        }
    }
    
    /*
     * You're wondering where the set methods are, right? Well, you're going
     * to have to remove the cadet and then create a new one, with new fields. HA.
     */
    
    //Cadet, Award, Base, Amount.
    //By doing this I am able to sort the awards, cadets, or how much they've earned.
    //IT's meant to be sorted yourself.
    
    private Category[] awardables = {Category.AA, Category.PT, Category.PA,
                                 Category.US, Category.CS, Category.DT,
                                 Category.CG, Category.RT, Category.OI,
                                 Category.BL, Category.SC};
    
    //transact and preview Awards return exactly the same thing. however,
    //transacting the awards adjusts the cap system so that it can stop awarding
    //subsequent awards.
    
    public ArrayList<Object[]> previewAwards()
    {
        ArrayList<Object[]> results = new ArrayList<Object[]>();
        
        for(Category c : awardables)
        {
            for(Cadet s : database.getCadets())
            {
                int al = s.getRibbonByCategory(c); //how much they already have
                int ts = calculateHowMuchToGive(s, c); //how much they're going to get
                                                   //from this moment we're holding a
                //hot potato; if we lose this data, we'e lost the evidence that a cadet
                //has actually earned this award. So, implementations of this class must
                //copy this array and avoid deleting information from it.
                
                //calculateHowMuchToGive doesn't perform the transaction.
                //i'm going to allow for a preview, then allow it to ask you
                //if you wanna make that permanent
                                                   
                if(ts >= 1)
                {
                    Object[] collection = new Object[4];
                    collection[0] = s;
                    collection[1] = c;
                    collection[2] = al;
                    collection[3] = ts; //usually ts should always be 1
                    results.add(collection);
                }
            }
        }
        System.out.println("[award] Award loading (preview) complete.");
        return results;
    }
    
    public ArrayList<Object[]> transactAwards()
    {
        ArrayList<Object[]> results = new ArrayList<Object[]>();
        
        for(Category c : awardables)
        {
            for(Cadet s : database.getCadets())
            {
                int al = s.getRibbonByCategory(c); //how much they already have
                int ts = transactCadetAward(s, c); //how much they're going to get
                                                   //from this moment we're holding a
                //hot potato; if we lose this data, we'e lost the evidence that a cadet
                //has actually earned this award. So, implementations of this class must
                //copy this array and avoid deleting information from it.
                                                   
                if(ts >= 1)
                {
                    Object[] collection = new Object[4];
                    collection[0] = s;
                    collection[1] = c;
                    collection[2] = al;
                    collection[3] = ts; //usually ts should always be 1
                    results.add(collection);
                }
            }
        }
        System.out.println("[award] Award transaction complete.");
        return results;
    }
    
    /*
     * This actually sets the command to actively change the records to award.
     */
    public int transactCadetAward(Cadet cadet, Category category)
    {
        int values = calculateHowMuchToGive(cadet, category);
        int alreadyHas = cadet.getRibbonByCategory(category);
        if(values >= 1)
        {
            cadet.setRibbon(category.getConstant(), values + alreadyHas);
            cadet.setSemester(category.getConstant(), 1);
            return values;
        }
        return 0;
    }
    
    /*
     * The function of this is to calculate HOW MUCH a cadet has earned, not to
     * actively award it. This is done in another method.
     */
    public int calculateHowMuchToGive(Cadet cadet, Category category)
    {
        int willEarn = calculateAwardsEarned(cadet, category);
        if(willEarn > 0) //we're going to award something
        {    
            if(cadet.getSemesterByCategory(category) == 0)
            {
                return 1;
            }
        }
        return 0;
    }
    
    private int calculateAwardsEarned(Cadet cadet, Category category)
    {
        int total = 0;
        int howManyCadetEarned = calculateTotalsFromEvent(cadet, category);
        if(category.equals(Category.CS) || category.equals(Category.US))
        {
            total = howManyCadetEarned / 10;
        }
        else if(category.equals(Category.PA))
        {
            total = howManyCadetEarned / 3;
        }
        else if(!category.equals(Category.FM) || !category.equals(Category.NO) ||
                !category.equals(Category.XX))
        {
            total = howManyCadetEarned;
        }
        return total;
    }
    
    private int calculateTotalsFromEvent(Cadet cadet, Category category)
    {
        int total = 0;
        for(Event e : database.getEvents())
        {
            if(e.getCategory().equals(category))
            {
                Integer value = e.get(cadet.getID());
                if(value != null && value.intValue() > 0)
                {
                    if(category.equals(Category.CS) || category.equals(Category.US)) 
                        total += value.intValue();
                    else
                        total++;
                }
            }
            else if(category.equals(Category.PA))
            {
                Integer value = e.get(cadet.getID());
                if(value != null && value.intValue() > 0)
                {
                    total++;
                }
            }
        }
        return total;
    }
}
